﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ProLinq.Mapping
{
	internal class MappingConfiguration<TSource, TResult> : MappingConfiguration
	{
		private readonly Mapper _mapper;
		public Dictionary<Object, Object> ObjectCache = new Dictionary<object, object>();

		public MappingConfiguration(Mapper mapper)
		{
			_mapper = mapper;
		}

		public Func<TSource, TResult> New { get; set; }
		public Func<TSource, TResult, TResult> Map { get; set; }

		public List<MemberMap> Maps { get; set; }

		protected override void CompileInternal()
		{
			var sw1 = new Stopwatch();
			sw1.Start();

			Type sourceType = typeof (TSource);
			Type resultType = typeof (TResult);

			MemberMap[] proprtyMap = CreatePropertyMap(sourceType, resultType)
				.Where(map => map.ResultMember is FieldInfo || ((PropertyInfo) map.ResultMember).CanWrite).ToArray();


			ParameterExpression param = Expression.Parameter(sourceType, "s");

			NewExpression newExpression = Expression.New(resultType);

			MethodInfo mapMethodTemplate =
				_mapper.GetType().GetMethods().First(m => m.Name == "Map" && m.GetParameters().Length == 1);


			Expression<Func<TSource, TResult>> lambda = Expression.Lambda<Func<TSource, TResult>>(newExpression, param);


			LabelTarget labelReturn = Expression.Label(resultType);
			ParameterExpression varResultExpression = Expression.Variable(resultType, "r");

			var assignExpressions = new List<Expression>();
			foreach (MemberMap map in proprtyMap)
			{
				MemberExpression sourceMember = Expression.MakeMemberAccess(param, map.SourceMember);
				MemberExpression destMember = Expression.MakeMemberAccess(varResultExpression, map.ResultMember);

				Type sourceMemberType = GetMemberType(map.SourceMember);
				Type destMemberType = GetMemberType(map.ResultMember);
				if (sourceMemberType == destMemberType)
				{
					BinaryExpression assign = Expression.Assign(destMember, sourceMember);
					assignExpressions.Add(assign);
				}
				else
				{
					MethodInfo mapMethod = mapMethodTemplate.MakeGenericMethod(sourceMemberType, destMemberType);

					MethodCallExpression mapCall = Expression.Call(Expression.Constant(_mapper), mapMethod, sourceMember);
					if (!sourceType.IsValueType && GetMemberType(map.SourceMember) == sourceType)
					{
						ConditionalExpression ifExpression = Expression.IfThenElse(
							Expression.ReferenceEqual(param, sourceMember),
							Expression.Assign(destMember, varResultExpression),
							Expression.Assign(destMember, mapCall)
							);
						assignExpressions.Add(ifExpression);
					}
					else
					{
						BinaryExpression assign = Expression.Assign(destMember, mapCall);
						assignExpressions.Add(assign);
					}
				}
			}

			BlockExpression blockExpression = Expression.Block(
				assignExpressions
					.Union(new Expression[] {Expression.Label(labelReturn, varResultExpression)})
				);
			Expression<Func<TSource, TResult, TResult>> lambda2 =
				Expression.Lambda<Func<TSource, TResult, TResult>>(blockExpression, param, varResultExpression);

			sw1.Stop();

			var sw2 = new Stopwatch();
			sw2.Start();
			New = lambda.Compile();
			Map = lambda2.Compile();
			sw2.Stop();
		}

		private Type GetMemberType(MemberInfo member)
		{
			if (member is FieldInfo)
				return ((FieldInfo) member).FieldType;
			if (member is PropertyInfo)
				return ((PropertyInfo) member).PropertyType;
			return null;
		}

		private IEnumerable<MemberInfo> GetPropertiesAndFields(Type type)
		{
			BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy;

			return from m in type.GetMembers(bindingFlags)
			       where m is PropertyInfo || m is FieldInfo
			       select m;
		}

		private IEnumerable<MemberMap> CreatePropertyMap(Type sourceType, Type resultType)
		{
			IEnumerable<MemberInfo> sourceMembers = GetPropertiesAndFields(sourceType);
			IEnumerable<MemberInfo> resultMembers = GetPropertiesAndFields(resultType);

			IEnumerable<MemberMap> maps = from s in sourceMembers
			                              let st = GetMemberType(s)
			                              let st1 = st == sourceType ? resultType : st
			                              join r in resultMembers on new {s.Name, Type = st1} equals
				                              new {r.Name, Type = GetMemberType(r)}
			                              select new MemberMap
				                              {
					                              SourceMember = s,
					                              ResultMember = r
				                              };


			return maps;
		}
	}
}